<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Topics tagged with vector 新手学习]]></title><description><![CDATA[A list of topics that have been tagged with vector 新手学习]]></description><link>http://forum.d2learn.org/tags/vector 新手学习</link><generator>RSS for Node</generator><lastBuildDate>Wed, 06 May 2026 02:04:34 GMT</lastBuildDate><atom:link href="http://forum.d2learn.org/tags/vector 新手学习.rss" rel="self" type="application/rss+xml"/><pubDate>Invalid Date</pubDate><ttl>60</ttl><item><title><![CDATA[从小白的视角探究 vector 第2章]]></title><description><![CDATA[<h2>2 改进 vector，实现 Big5</h2>
<blockquote>
<p dir="auto">这部分内容我们依旧使用sunrisepeak大佬的代码演示，但在后半段，我们将会深入一些内容</p>
</blockquote>
<h3>2.1 vector需要什么？</h3>
<blockquote>
<p dir="auto">经过第一章的描述，我们现在应该思考vector需要什么，由此我们才能得知vector需要实现什么</p>
</blockquote>
<ol>
<li><code>整块空间</code> 类比函数栈，它由<strong>空间适配器 Allocator</strong>提供，<code>申请</code><strong>一整块内存</strong></li>
<li><code>表现的像数组</code> vector需要一个size，标定数组的边界</li>
<li><code>额外的空间</code>  vector是一个动态的数组，它标志着vector的空间可以调整大小，因此需要一个<strong>容量capacity</strong></li>
</ol>
<ul>
<li>额外的空间，并不是再申请一块内存，而是在<code>整块内存内部</code>，需要<strong>区分哪些已经使用，哪些未使用且将来可以使用</strong>，一旦突破这个界限，就需要再申请一大块内存进行<code>扩容</code></li>
</ul>
<p dir="auto">经过以上总结，再回看第一章节的内容，可以明显发现一点，只有一个size，这个size<strong>既标定了已经使用的数量，又标定了vector能容纳的最大元素数量</strong>。这显然是不合理的，即便我们需要把这块空间内都初始化出对象，但依然缺少一个逻辑上能区分已使用量和未使用量的内容。</p>
<h3>2.2 添加容量</h3>
<blockquote>
<p dir="auto">代码方面我尽量复用之前的，大体讲解可以参照对应的视频讲解，后续讲解我只会说明一些我认为初学者容易不理解的地方</p>
</blockquote>
<pre><code class="language-cpp">template &lt;typename T, typename Alloc = DefaultAllocator&gt;
class Vector {
public:
    // 此处添加mCapacity_e的初始化
    Vector() : mSize_e { 0 }, mCapacity_e { 0 }, mDataPtr_e { nullptr } { }

    Vector(int size) : mSize_e { 0 }, mCapacity_e { size } {
        // 1. 注意点1
        mDataPtr_e = static_cast&lt;T *&gt;(Alloc::allocate(sizeof(T) * mCapacity_e));
        // 2. 注意点2
        for (int i = 0; i &lt; mCapacity_e; i++, mSize_e++) {
            new (mDataPtr_e + i) T();
        }
    }
    ~Vector() {
        if (mSize_e) {
            // 3. 注意点3
            for (int i = 0; i &lt; mSize_e; i++) {
                (mDataPtr_e + i)-&gt;~T();
            }
            Alloc::deallocate(mDataPtr_e, mCapacity_e * sizeof(T));
        }
    }

private:
    int mSize_e;
    int mCapacity_e;
    T * mDataPtr_e;
};

</code></pre>
<p dir="auto">可以看到，大框架基本不变，但是添加了容量后，依旧需要注意语句变化带来的含义</p>
<ol>
<li><code>申请空间以容量为标定</code></li>
<li><code>初始化一个元素mSize_e就需要增长一个</code></li>
<li><code>元素析构以mSize_e为边界</code></li>
</ol>
<ul>
<li>再次对比到函数空间，此时我们就可以理解为什么需要用到<code>new (mDataPtr_e + i) T();</code> 这样的技巧</li>
</ul>
<ol>
<li>在函数中初始化对象，我们可以直接写下 <code>T aaa</code>，<strong>不用我们自己管理这个对象在函数空间内部的哪个地方</strong></li>
<li>在vector中，没有这种机制，让对象挨个排列在内部空间中，因此，我们需要手动告诉程序，我们需要在 <code>new(地址) 这个位置</code>，<code>初始化 T() 对象</code> 。正是这个不同之处，需要我们使用到placement new，也就是定位。如果有自动的机制，我们<strong>甚至可以直接和在函数内部初始化对象一样</strong>，丝毫不用关心在函数空间的哪里有这个对象！</li>
</ol>
<blockquote>
<p dir="auto">如何销毁内容？</p>
</blockquote>
<ul>
<li>类比到函数栈中，我们已知函数在退出时会<strong>自动调用对象的析构函数</strong>，因此我们希望vector也在它自身析构时，<strong>显式调用已构建对象的析构函数</strong></li>
<li>之所以不需要类似placement new这样的操作，是因为在创建时，需要强制确定内存位置，<strong>但是销毁时，我们已经知道了哪些内容已经存在</strong>，可以显式直接调用。</li>
</ul>
<h4>capacity的作用</h4>
<blockquote>
<p dir="auto">capacity的加入，在目前还看不出区别，这是因为这几个构造函数都是直接让size和capacity相等，但是后续一旦需要再添加元素，产生扩容，那么就会体现出区别。<br />
引入capacity后，我们就需要在脑海中将<strong>整块内存划分为不同的区域</strong></p>
</blockquote>
<ol>
<li><code>已构建对象的内存</code>   <code>[data, size)</code>     这部分内容上已经有了对象，可以看到析构也是以此为界限</li>
<li><code>未构建对象的内存</code>   <code>[size, capacity)</code> 这部分没有任何内容，构建新内容必须使用<code>placement new</code>这样的技术</li>
<li>需要再次强调placement new技术，只有在<code>裸内存</code>上才能使用，也就是如果已经在一个地方用此技术初始化了一个对象，那么<strong>再次使用placement new在同一位置创建一个新内容是不允许的</strong>！除非<strong>先调用析构函数，将对象销毁，变为裸内存的状态</strong>才能继续使用。这部分对于新手来说直接理解依旧是非常抽象的，并且这个行为是<strong>未定义行为</strong>，也就是编译器不会给你强制警告！<br />
类比到函数中，那就是<strong>无法第二次调用对象的构造函数进行初始化</strong>！因为构造就已经开启了生命周期，再次使用会产生一些列问题，比如内存泄漏，多次析构等等。<pre><code class="language-mermaid">graph TD
subgraph 正确方式
    A[已有元素位置: 活对象 T] --&gt; B[调用赋值运算符 *p = val]
    B --&gt; C[对象值更新，无泄漏 ✅]
end

subgraph 错误方式
    A --&gt; D[❌ 直接 placement new 覆盖]
    D --&gt; E[旧对象未析构，资源泄漏&lt;br/&gt;或二次析构风险（未定义行为）]
end
</code></pre>
<pre><code class="language-mermaid">graph TD
subgraph 正确方式
    S[栈上对象 T x 已构造] --&gt; T[使用赋值 x = val]
    T --&gt; U[对象值更新 ✅]
end

subgraph 错误方式
    S --&gt; V[❌ 尝试第二次调用构造函数]
    V --&gt; W[编译错误：不能直接调用构造函数&lt;br/&gt;或 显式析构后 placement new&lt;br/&gt;导致作用域结束时二次析构]
end
</code></pre>
</li>
<li>接上一条，那么在某一位置已有对象的情况下，除了调用析构函数将对象销毁，变成裸内存，另外的方法就是调用<strong>赋值运算符</strong>，以更新内容，包括拷贝赋值和移动赋值。这也和我们在函数内使用 = 进行赋值和更新是<strong>同一个道理</strong>。</li>
</ol>
<pre><code class="language-mermaid">flowchart LR
    subgraph stack_frame[函数栈帧]
        V[vector对象\n data / size / capacity]
    end

    subgraph heap_buffer[堆上的连续存储]
        E1[元素0]
        E2[元素1]
        S1[空槽位]
    end

    V --&gt; E1
    V --&gt; E2
    V --&gt; S1
</code></pre>
<pre><code>data                     data+size               data+capacity
  ↓                         ↓                         ↓
  ┌─────────────────────────┬─────────────────────────┐
  │   已构造对象区           │    未构造预留区          │
  └─────────────────────────┴─────────────────────────┘
</code></pre>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="text-align:left">区域</th>
<th style="text-align:left">地址范围</th>
<th style="text-align:left">对象状态</th>
<th style="text-align:left">可执行操作</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">已构造对象区</td>
<td style="text-align:left"><code>[data, data+size)</code></td>
<td style="text-align:left">存在活对象</td>
<td style="text-align:left">• 改变内容：用<strong>拷贝/移动赋值</strong>&lt;br&gt;• 若必须整体替换：<strong>先析构，再 placement new</strong>（一般不推荐）</td>
</tr>
<tr>
<td style="text-align:left">未构造预留区</td>
<td style="text-align:left"><code>[data+size, data+capacity)</code></td>
<td style="text-align:left">原始内存（无对象）</td>
<td style="text-align:left">• 创建对象：<strong>必须通过 placement new 构造</strong>&lt;br&gt;• 无“修改”一说，因为没有对象可以修改</td>
</tr>
</tbody>
</table>
<blockquote>
<p dir="auto">这里也可以看出我们类比函数栈的好处，那就是不必拘泥于抽象的、高深的内容，不用一开始就去理解STL标准库的思想<br />
毕竟这是该领域顶级专家的成果，想要短时间内容入门是非常困难的。<br />
但是一旦我们将它和我们日常使用的内容相连，我们就可以通过<strong>共性</strong>去理解其原理，通过不同之处的探究，带着问题探究其深意</p>
</blockquote>
<h3>2.3 全部构造函数</h3>
<blockquote>
<p dir="auto">此处不会详细讲解3/5/0法则，只需要知道如果我们显式定义了析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符，其中的任意一项，就需要把这几项全部定义出来（或者删除掉）<br />
简而言之，如果有<strong>任意一项资源需要我们手动管理</strong>，这几个函数就必须认真对待。因为有手动管理，才需要定义这些内容，而这些内容一旦定义任意一项，移动相关的内容就不会由编译器自动生成，其余的哪怕生成，也是有问题的！</p>
</blockquote>
<h4>1 构造函数的类型</h4>
<blockquote>
<p dir="auto">此处简单讲解一下不同类型构造函数的作用和区别，以及一些隐式的条件</p>
</blockquote>
<ol>
<li><code>默认</code>构造函数  <strong>不需要参数</strong>就能调用，用于 <strong>ClassT t</strong> 或 <strong>ClassT t{}</strong> 这样声明一个对象，没有参数<br />
同时这个函数还有一系列的规则，包括自动生成、被抑制生成等</li>
<li><code>普通</code>构造函数  <strong>需要提供参数</strong>才能调用 用于 <strong>ClassT t(10)</strong> 这样的调用<br />
之所以区分，是因为默认构造函数有一系列<code>特殊规则</code></li>
<li><code>复制</code>构造函数  <strong>深拷贝</strong> 用于<strong>ClassT t_copy(other)<strong>这样的形式  对于</strong>有手动管理资源</strong>的类，不能使用编译器提供的复制构造，必须自己实现深拷贝，否则<code>只会拷贝指针</code>这样的门牌号，指向同一块内存，产生<code>双重释放</code>的问题</li>
<li><code>移动</code>构造函数  <strong>窃取资源</strong> 用于<strong>ClassT t_copy(std::move(other))<strong>这样的形式 将资源从别的对象</strong>转移到自身</strong>，并且需要切断别的对象对于资源的所有权</li>
</ol>
<ul>
<li>注：<strong>不需要参数</strong> <code>不代表没有参数</code>，因为还涉及到<strong>默认值</strong>等问题。且以上调用形式可能存在部分问题，需要后续研究。这里研究的重点还是这几个内容如何实现</li>
<li>注2：std::move 只是代表将对象转换为<strong>右值引用</strong>的形式，让我们能够调用移动构造函数，但<strong>并没有字面意义上的移动功能</strong></li>
</ul>
<h5>1 默认构造函数</h5>
<blockquote>
<p dir="auto">在上文中，<code>Vector() : mSize_e { 0 }, mCapacity_e { 0 }, mDataPtr_e { nullptr } { }</code> 就是一个默认构造函数，但是这个函数被我们<strong>显式声明</strong>了，因为声明了一般的构造函数后，编译器是不会再生成默认构造函数的，除非我们自己再写出来</p>
</blockquote>
<blockquote>
<p dir="auto">那这样的函数还是默认构造函数吗？</p>
</blockquote>
<pre><code class="language-cpp">// 注意此处的参数
Vector(int size = 0) : mSize_e { 0 }, mCapacity_e { size } {
    mDataPtr_e = static_cast&lt;T *&gt;(Alloc::allocate(sizeof(T) * mCapacity_e));
    for (int i = 0; i &lt; mCapacity_e; i++, mSize_e++) {
        new (mDataPtr_e + i) T();
    }
}
</code></pre>
<ul>
<li>这依然是一个默认构造函数</li>
</ul>
<ol>
<li>依旧是前文所讲内容，size有一个<code>默认值</code>，<strong>无需参数</strong>就能调用，符合规则</li>
<li><strong>默认值</strong> 需要以后额外补充，大家也可以自行查找相关资料了解</li>
<li>同时这个默认构造函数也可以提供参数调用，但这个问题暂时无法深入</li>
</ol>
<h5>2 普通构造函数</h5>
<blockquote>
<p dir="auto">此处划分这两个构造函数的方式可能有问题，希望大家可以指出</p>
</blockquote>
<ul>
<li><code>Vector(int size) : mSize_e { 0 }, mCapacity_e { size }</code> 完整代码参照上文中出现的</li>
</ul>
<ol>
<li>这里的size决定了我们调用的时候必须给出参数才能匹配到该函数，比如用{} 初始的形式，Vector v1{10}; 10代表了初始的容量，以及将这10个空位都初始化上对应的对象</li>
<li><code>: mSize_e { 0 }</code> 使用了C++11提供的初始化器，可以方便地将成员进行初始化，避免遗漏等问题。需要注意的是，初始化的顺序并不由我们写的顺序决定，不考虑继承等情况下，由<strong>成员在声明时的顺序</strong>决定</li>
</ol>
<h5>3 复制构造函数</h5>
<blockquote>
<p dir="auto"><code>为什么需要深拷贝？</code></p>
</blockquote>
<ol>
<li>
<p dir="auto"><strong>指针</strong> 这是一切问题的根源，编译器生成的复制构造，只会去<code>复制指针的值</code>，这一段同样对应<code>第一章补充内容</code>，正如函数作用域不会管理指针背后的资源，默认生成的拷贝构造也不会去考虑远在天边的内存。</p>
</li>
<li>
<p dir="auto"><strong>内存复制</strong> 拷贝的主要意义是对<strong>资源的拷贝</strong>，简单指针复制是无效的，必须手动提供对资源内存的复制，我们希望得到的是<strong>另一块独立的内存资源</strong>，而不是当前资源的影子。</p>
</li>
</ol>
<blockquote>
<p dir="auto">由此，我们的目标也就很明确了，申请一块<strong>大小相同的内容</strong>，并在其上<strong>一一复制对象</strong></p>
</blockquote>
<pre><code class="language-cpp">Vector(const Vector&amp; other) : mSize_e {0}, mCapacity { other.mCapacity } {
    mDataPtr_e = static_cast&lt;T *&gt;(Alloc::allocate(sizeof(T) * mCapacity));
    for (int i = 0; i &lt; other.mSize_e; i++, mSize_e++) {
        new (mDataPtr_e + i) T(other.mDataPtr_e[i]);
    }
}
</code></pre>
<ul>
<li><strong>接收参数</strong> <code>const Vector&amp;</code>  复制复制，首先就是要有<code>另一个对象</code>，因此采用<code>引用</code>的方式，同时<strong>不会修改也不允许修改</strong>另一个对象的内容，因此使用<code>const</code>修饰</li>
<li><strong>size capacity</strong> 可以看出，这二者是有区别的，再次反映出了前文的内容。整体容量和已构造元素是要区分开的</li>
<li><strong>placement new</strong> <code>new (mDataPtr_e + i) T(other.mDataPtr_e[i]);</code> 需要注意，我们依旧是在裸内存上进行构造，因此仍然需要使用定位new技术</li>
</ul>
<h5>4 移动构造函数</h5>
<blockquote>
<p dir="auto"><code>快！</code></p>
</blockquote>
<pre><code class="language-cpp">Vector(Vector &amp;&amp;other) : mSize_e { other.mSize_e }, mCapacity { other.mCapacity }, mDataPtr_e {other.mDataPtr_e} noexcept {
    // reset
    other.mSize_e = 0;
    other.mCapacity = 0;
    other.mDataPtr_e = nullptr;
}
</code></pre>
<ul>
<li>
<p dir="auto"><strong>接收参数</strong> <code>Vector &amp;&amp;other</code>  移动也是要有<code>另一个对象</code>，因此采用<code>右值引用</code>的方式，同时<strong>必须修改</strong>另一个对象的内容，所以<code>无需const修饰</code></p>
</li>
<li>
<p dir="auto"><strong>浅拷贝</strong> 可以看到移动的本质是<strong>浅拷贝</strong>，但是之前在复制构造时说不能使用浅拷贝，但是此处又使用了浅拷贝，这是为什么？</p>
<pre><code class="language-cpp">    // reset
other.mSize_e = 0;
other.mCapacity = 0;
other.mDataPtr_e = nullptr;
</code></pre>
<p dir="auto">原因在于，我们 “抛弃” 了拿来移动的那个对象，更准确地说，我们宣布另一个对象<strong>不再持有资源</strong>了。而之前的复制构造，需要两个对象都有资源，这才是关键区别</p>
</li>
<li>
<p dir="auto"><strong>快</strong> 快是最关键的好处，因为只复制几个指针和大小容量等内容，是非常迅速的，但是代价就是另一个vector对象失去了资源</p>
</li>
</ul>
<blockquote>
<p dir="auto">关于<strong>右值引用</strong>请大家自行查阅相关内容。其实就是为了区别调用的函数，调用移动必须要用这种方式。其他方式依旧可以完成这种内部资源转移，<strong>但是为了语义上的区分</strong>，这样做是最好的</p>
</blockquote>
<h3>2.4 特殊运算符 =</h3>
<blockquote>
<p dir="auto">注意，此处的主体是运算符，而不是构造函数</p>
</blockquote>
<ul>
<li>既然是运算符，区别于构造函数，那就意味着vector对象<strong>已经经历过调用构造函数的阶段</strong>，也就是<strong>初始化成功后</strong>，才能调用运算符</li>
<li>既然已经初始化过了，说明对象内部是<strong>有内容的</strong></li>
<li>运算符可以出现自赋值的情况 比如 <code>vec1 = vec1</code></li>
</ul>
<blockquote>
<p dir="auto">以上的注意点，构成了我们接下来两个特殊运算符的要点</p>
</blockquote>
<h4>运算符类型</h4>
<ol>
<li><code>复制赋值运算符</code> 类似赋值构造函数</li>
<li><code>移动赋值运算符</code> 类似移动构造函数</li>
</ol>
<ul>
<li>调用方式都相同，使用 <code>=</code> 调用，但是同样，需要用不同的方式去匹配不同的运算符</li>
<li><code>重写operator=()</code> 其中最重要的就是()内参数的选择</li>
<li>需要有<code>返回值</code></li>
</ul>
<h5>1 复制赋值运算符</h5>
<blockquote>
<p dir="auto">接下来，我们需要根据代码，来分析上文提到的特殊要点</p>
</blockquote>
<pre><code class="language-cpp">// 1
Vector&amp; operator=(const Vector &amp;other) {
    // 2
    this-&gt;~Vector();
    mSize_e = other.mSize_e;
    mCapacity = other.mCapacity;
    mDataPtr_e = static_cast&lt;T *&gt;(Alloc::allocate(sizeof(T) * mCapacity));
    for (int i = 0; i &lt; mSize_e; i++) {
        mDataPtr_e[i] = other.mDataPtr_e[i];
    }
    // 3
    return *this;
}
</code></pre>
<blockquote>
<p dir="auto">可以看出，复制赋值运算符，基本沿袭了赋值构造的思路，但是多了释放自身内容和返回的步骤</p>
</blockquote>
<ol>
<li>返回值是vector&amp;</li>
<li>既然已经初始化过，有内容，就必须<strong>先释放掉vector对象本身的内容</strong>，因此调用了<strong>vector本身的析构函数</strong></li>
<li>对this指针，解引用，获得vector对象，匹配上引用返回</li>
</ol>
<ul>
<li>this指针内容需要大家自行查阅</li>
</ul>
<blockquote>
<p dir="auto">以上的代码符合了<strong>需要释放自身旧内容、返回对象</strong>。但是依旧是有问题的，可以看出漏掉了最关键的自赋值的问题</p>
</blockquote>
<blockquote>
<p dir="auto">倘若出现 <code>vec1 = vec1</code> 这样的代码，那么调用之后，在<strong>this-&gt;~Vector();</strong> 就把自身内容全部释放了，也就是说 vec1 现在变成了一个<code>空对象</code>，<code>所持有的资源消失</code>，这显然不是我们希望看到的</p>
</blockquote>
<p dir="auto">如何做呢？ 非常简单，也就是如果判断传进来的对象是自身，那就直接返回，否则就执行销毁自身内容并复制的步骤</p>
<pre><code class="language-cpp">Vector&amp; operator=(const Vector &amp;other) {
    // 注意
    if(this != &amp;other) {
        this-&gt;~Vector();
        mSize_e = other.mSize_e;
        mCapacity = other.mCapacity;
        mDataPtr_e = static_cast&lt;T *&gt;(Alloc::allocate(sizeof(T) * mCapacity));
        for (int i = 0; i &lt; mSize_e; i++) {
            mDataPtr_e[i] = other.mDataPtr_e[i];
        }
    }
    return *this;
}
</code></pre>
<ol>
<li><strong>this != &amp;other</strong> this是指向对象本身的指针，而对对象使用 &amp;，取得了地址，也就相当于this，因此此处是<strong>地址的比较</strong></li>
<li>之所以比较地址，<strong>而不是比较对象是否相等</strong>，是因为这样最简单，而且比较对象，是需要重写比较相关的运算符才可以进行的</li>
</ol>
<h5>2 移动赋值运算符</h5>
<pre><code class="language-cpp">Vector&amp; operator=(Vector &amp;&amp;other) noexcept {
    // 注意
    if(this != &amp;other) {
        this-&gt;~Vector();
        mSize_e = other.mSize_e;
        mCapacity = other.mCapacity;
        mDataPtr_e = other.mDataPtr_e;

        other.mSize_e = 0;
        other.mCapacity = 0;
        other.mDataPtr_e = nullptr;
    }
    return *this;
}
</code></pre>
<ul>
<li>可以看出，移动赋值运算符也是类似的结构</li>
</ul>
<h3>2.5 总结</h3>
<blockquote>
<p dir="auto">这段将简单总结一下第二章的内容</p>
</blockquote>
<ol>
<li>首先我们模仿了函数的效果，但是<code>手动进行了内存管理</code>，并且<code>手动控制了在这块内存上对象的生命周期</code></li>
<li>解耦了已有元素数量（size）和容量（capacity），这要求我们在构造函数和析构函数以及特殊运算符中，<strong>申请空间需要以capacity为大小</strong>，<strong>控制对象生命周期，需要以size为大小</strong></li>
<li>构造函数和特殊运算符的规则，使得我们必须手动实现资源的管理，实现由编译器自动生成的内容无法实现的功能</li>
</ol>
<blockquote>
<p dir="auto">但是，显然这部分内容是非常<code>浅显</code>的，大部分的讲解都会提到这些内容，也不过是用几个新技术，在堆内存上模拟了一个数组的功能，并且我们大部分的接口还没有实现。如何再深入进去呢？如何理解标准库的一些思想呢？</p>
</blockquote>
<blockquote>
<p dir="auto">我们要学习vector，不仅是要学习代码怎么写，怎么实现，更要学习它的思想，需要在代码中抽丝剥茧，提炼出能体现某些原则的东西。</p>
</blockquote>
<h4>关键！！！！</h4>
<blockquote>
<p dir="auto">可以注意到，以上的部分函数，添加了一个叫<strong>noexcept</strong>的东西<br />
它声明了，该函数不会抛出<code>异常</code></p>
</blockquote>
<ul>
<li><strong>异常是我们串联起后续内容，拆解stl思想的一个重要切入点</strong></li>
<li>没有异常，我们是难以理解为什么vector的代码如此复杂，复杂的同时还有<strong>一系列的原则</strong>，仿佛<strong>数据库一样</strong></li>
</ul>
<h2>3 noexcept 引发的血案</h2>
<p dir="auto">// TODO</p>
<hr />
<h2>感谢</h2>
<ul>
<li>本文代码的核心框架来自 bilibili LH_Mouse大佬的<a href="https://www.bilibili.com/video/BV1iX4y1w7x4/" rel="nofollow ugc">视频</a>。</li>
<li>同时也参考了 sunrisepeak 大佬的代码和视频: <a href="https://www.bilibili.com/video/BV1K1421z7kt/" rel="nofollow ugc">BV1K1421z7kt</a>。且本文大部分讲解代码直接用的对应的教学文档代码。</li>
</ul>
]]></description><link>http://forum.d2learn.org/topic/205/从小白的视角探究-vector-第2章</link><guid isPermaLink="true">http://forum.d2learn.org/topic/205/从小白的视角探究-vector-第2章</guid><dc:creator><![CDATA[dustchens]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[从小白的视角探究 vector 第一章补充内容]]></title><description><![CDATA[<h2>1-补充内容</h2>
<h3>1 对象到底在哪？</h3>
<blockquote>
<p dir="auto">在C++中，不管是基本类型还是类，都可以在栈和堆上</p>
</blockquote>
<pre><code class="language-cpp">struct MyClass{
    int num = 0;
}
signed main() {
    // 栈上，也就是在main函数内部的空间里
    MyClass m1;
    int i1 = 100;
    // 创建的对象在堆内存中，但是提供了一个门牌号给你访问对应的内容
    // 但是m2这个指针变量本身还是在栈空间上。
    MyClass* m2 = new MyClass;
    int* i2 = new int(100);
    return 0;
}
</code></pre>
<p dir="auto">由这段代码，我们可以有两个认识</p>
<ol>
<li>对象可以在<code>任何位置</code>，不管是函数的栈空间内，还是在堆内存上</li>
<li>在堆内存中的对象，<code>函数内只有它的指针</code>。但指针这个内容本身，还是<strong>一个在栈上的内容</strong></li>
</ol>
<blockquote>
<p dir="auto">正是这个指针的存在，割裂了我们对用new申请出来内容的认知，也让大部分初学RAII的人对如何运用智能指针产生了犹豫</p>
</blockquote>
<ul>
<li>RAII要求资源需要绑定到<code>对象</code>上，用对象的生命周期去管理那片申请出来的资源
<ul>
<li>首先，许多初学者只看到了如何编写<code>构造函数</code>和<code>析构函数</code>这些内容，但是没有认识到，<strong>对象</strong>才是RAII产生作用的<strong>主体</strong>！</li>
<li>其次，既然是<strong>对象</strong>，那就<strong>必须是对象</strong>！ 是的，这是一句废话！</li>
<li>错误的，<code>这不是一句废话</code>！很神奇对吧。在上面的例子中，尽管我们申请了4个内容，但是只有两个是对象<br />
分别是 <strong>MyClass m1; 和 int i1 = 100</strong>; 那么另外两个对象在哪呢？<br />
是的，我们根本没 <strong>m2和i2</strong> <code>指向对象</code>的<code>直接控制权</code> 这就是两个破烂牌子，不是<strong>对象本身</strong>！</li>
</ul>
</li>
</ul>
<pre><code class="language-mermaid">flowchart LR
    subgraph S[main 作用域]
        m1[MyClass m1&lt;br/&gt;对象本体]
        i1[int i1&lt;br/&gt;对象本体]
        p1[MyClass* m2&lt;br/&gt;指针变量]
        p2[int* i2&lt;br/&gt;指针变量]
    end

    subgraph H[动态存储区]
        o1[new MyClass&lt;br/&gt;对象本体]
        o2["new int(100)&lt;br/&gt;对象本体"]
    end

    p1 --&gt; o1
    p2 --&gt; o2
</code></pre>
<p dir="auto">通过以上的讲解，大伙终于发现了盲点，我们一切申请内存得到的内容，<strong>都只是指针</strong>，而<strong>不是对象本身</strong>，对象本身不在了，那我们就无法用它的任何内容去进行资源的处理(RAII)</p>
<ul>
<li>之前在<code>1.3章的3节</code>中，讲过用智能指针进行<code>套娃</code>，套娃的本质就是，我们需要<strong>对象</strong>！<code>new</code>得到的不是对象，因此我们需要再把它交给一个托管的对象，也就是 std::unique_ptr&lt;Classxxxxx&gt; 对象uuuuu(new Classxxxxx) ，这里唯一有用的只有这个 <code>对象uuuuu</code>。正是这个不是指针的美好东西，给了我们一切随意申请空间和资源的权力。</li>
</ul>
<p dir="auto">以上的内容我相信可以帮助初学者建立起对RAII的认知，之所以不强调如何<strong>对资源进行封装</strong>，而是强调对象，是要破除初学者对指针的迷茫和恐惧，<strong>一切申请资源却没有直接得到对象的内容</strong>，都需要再用智能指针包裹，<strong>即使我们申请资源的类本身已经在构造函数和析构函数里对资源进行了封装</strong>。当我们认识到了这一点，那么我们就正确知道了何时需要使用智能指针。</p>
<h3>2 栈空间和作用域</h3>
<blockquote>
<p dir="auto">好吧，虽然标题有作用域，但还是不太想讲<br />
此处不做复杂的讲解，而仅仅是一个简单类比，这个类比对于我们编写vector并理解其思想至关重要。</p>
</blockquote>
<p dir="auto">当一个函数被调用时，通常会形成一个新的 <strong>栈帧（stack frame）</strong>。你可以把它先粗略理解成“这次函数调用专属的一小块工作区”。它里面经常会放这些东西：</p>
<ul>
<li>返回地址等调用信息</li>
<li>函数参数</li>
<li>局部变量</li>
<li>某些临时对象</li>
</ul>
<blockquote>
<p dir="auto"><strong>函数调用提供了一个自动管理的生存边界。对象在这个边界里创建，在边界结束时被清理。</strong> 这个边界也就是作用域</p>
</blockquote>
<p dir="auto">函数应该是每一个学习编程的人能够使用到的最简单、无副作用的内容，在这块系统为我们提供的区域中，我们写的内容（不涉及申请资源）会被这块区域自动管理和销毁。我们丝毫没有意识到，在函数中写下 int a = 10; 其中既涉及到使用了空间，又涉及到函数结束后的自动清理。倘若堆内存也有这样的功能，那么程序员在申请内存时将会毫无包袱。</p>
<ul>
<li>
<p dir="auto">得益于离开作用域后自动调用的析构函数，我们得以使用RAII这样的<strong>桥梁</strong>，链接到管理的堆内存空间中，销毁资源</p>
</li>
<li>
<p dir="auto">以下是函数栈和vector的相似之处</p>
</li>
</ul>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>函数栈模型</th>
<th>vector 模型</th>
</tr>
</thead>
<tbody>
<tr>
<td>函数调用开始，得到一块受管理的生存边界</td>
<td>vector 构造完成，内部得到一块和vector对象绑定的空间</td>
</tr>
<tr>
<td>在作用域里声明局部变量</td>
<td>在连续存储里构造元素</td>
</tr>
<tr>
<td>作用域结束，局部对象自动析构</td>
<td>vector 析构时，元素自动析构</td>
</tr>
<tr>
<td>栈帧整体被回收</td>
<td>底层连续存储被释放</td>
</tr>
</tbody>
</table>
<pre><code class="language-mermaid">flowchart LR
    subgraph F[函数作用域]
        f1[进入作用域]
        f2[创建局部对象]
        f3[离开作用域]
        f4[对象自动析构]
        f1 --&gt; f2 --&gt; f3 --&gt; f4
    end

    subgraph V[vector 对象]
        v1[构造 vector]
        v2[申请一块存储]
        v3[在槽位上构造元素]
        v4[析构 vector]
        v5[元素析构并释放存储]
        v1 --&gt; v2 --&gt; v3 --&gt; v4 --&gt; v5
    end
</code></pre>
<blockquote>
<p dir="auto"><strong>vector需要显式地提供 申请空间 管理内部对象生命周期</strong><br />
栈则自动提供了空间且能够自动管理内部对象生命周期</p>
</blockquote>
<ol>
<li>函数栈帧通常是这次调用<code>固定</code>的一块区域；vector <strong>可以扩容、更换区域地址</strong>。</li>
<li>函数的<code>自动清理</code>由语言规则提供；vector 的清理由<strong>类的构造/析构逻辑</strong>提供。</li>
<li>栈上的局部对象通常一声明就开始生命周期；vector 可以先只有原始存储，再逐个决定哪些槽位真的构造成对象。</li>
<li>当然，得益于RAII，栈空间和vector都能通过调用对象析构函数，销毁对象自己持有的资源。只不过一个是自动，一个需要手动</li>
</ol>
<hr />
<p dir="auto">对于这样的一个内容</p>
<pre><code class="language-cpp">// 没有实现RAII
struct Vec {
    Vec(int s) {
        size = s;
        data = new int(xxx);
    }
    int size;
    int *data; 
};

signed main() {
    Vec v{10};
    return 0;
}
</code></pre>
<pre><code class="language-mermaid">flowchart LR
    subgraph S[main 作用域]
        v["Vec v&lt;br/&gt;size: 10&lt;br/&gt;data"]
    end
    subgraph H[动态存储区]
        arr["new int[10] 内存块&lt;br/&gt;（泄漏！）"]
    end
    v -- data 指针 --&gt; arr
    style arr fill:#ffcccc,stroke:#ff0000
</code></pre>
<ul>
<li>可以看出，<strong>直接创建对象</strong>，<strong>没有使用RAII技术</strong>，内存泄漏了</li>
</ul>
<blockquote>
<p dir="auto">使用RAII后</p>
</blockquote>
<pre><code class="language-cpp">// 实现RAII
struct Vec {
    Vec(int s) {
        size = s;
        data = new int(xxx);
    }
    ~Vec() {
        delete data;
    }
    int size;
    int *data; 
};

signed main() {
    Vec v{10};
    return 0;
}
</code></pre>
<pre><code class="language-mermaid">flowchart LR
    subgraph S[main 作用域]
        v2["Vec v&lt;br/&gt;size: 10&lt;br/&gt;data&lt;br/&gt;（析构函数自动 delete[]）"]
    end
    subgraph H[动态存储区]
        arr2["new int[10] 内存块&lt;br/&gt;（自动释放）"]
    end
    v2 -- data 指针 --&gt; arr2
    style arr2 fill:#ccffcc,stroke:#00aa00
</code></pre>
<blockquote>
<p dir="auto">通过<code>对象</code>和<code>RAII</code>，<code>自动释放</code>了</p>
</blockquote>
<blockquote>
<p dir="auto">以下是使用RAII，即使使用了new将vec对象放在了堆内存上，但是通过托管，也成功释放了</p>
</blockquote>
<pre><code class="language-cpp">signed main() {
    std::unique_ptr&lt;Vec&gt; up(new Vec(10));
    return 0;
}
</code></pre>
<pre><code class="language-mermaid">flowchart LR
    subgraph S[main 作用域]
        up["std::unique_ptr&amp;lt;Vec&amp;gt; up&lt;br/&gt;（持有 Vec*）"]
    end
    subgraph H[动态存储区]
        vecobj["new Vec&lt;br/&gt;size: 10&lt;br/&gt;data"]
        arr3["new int[10] 内存块&lt;br/&gt;（由 Vec 析构释放）"]
    end
    up -- 管理 --&gt; vecobj
    vecobj -- data 指针 --&gt; arr3
    style vecobj fill:#ccffcc,stroke:#00aa00
    style arr3 fill:#ccffcc,stroke:#00aa00
</code></pre>
<ul>
<li>双重自动释放：unique_ptr 析构 → delete vecobj → Vec::~Vec() → delete[] arr3，所有资源全部安全回收。</li>
</ul>
<hr />
<ul>
<li>以上类比最大程度关注了两者的相似之处。真正的难点在于二者不同的地方。这些不同的内容，也将是这篇文章对于一般的vector教学最大的不同之处。虽然我这个也挺一般的.....</li>
</ul>
]]></description><link>http://forum.d2learn.org/topic/204/从小白的视角探究-vector-第一章补充内容</link><guid isPermaLink="true">http://forum.d2learn.org/topic/204/从小白的视角探究-vector-第一章补充内容</guid><dc:creator><![CDATA[dustchens]]></dc:creator><pubDate>Invalid Date</pubDate></item></channel></rss>